package top.milkbox.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.multipart.MultipartFile;
import top.milkbox.domain.User;
import top.milkbox.info.TokenUserInfo;
import top.milkbox.info.UserAvatar;
import top.milkbox.service.UserService;
import top.milkbox.mapper.UserMapper;
import org.springframework.stereotype.Service;
import top.milkbox.utils.GuoUtilsDate;
import top.milkbox.utils.GuoUtilsFile;
import top.milkbox.utils.GuoUtilsRandom;
import top.milkbox.utils.GuoUtilsSort;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author xiaoguo
 * @description 针对表【tb_user】的数据库操作Service实现
 * @createDate 2022-09-10 21:08:48
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    private UserMapper userMapper;

    // 用户头像URL类
    @Autowired
    private UserAvatar userAvatar;

    @Autowired
    private TokenUserInfo tokenUserInfo;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    protected ObjectMapper objectMapper;

    /**
     * 预处理用户信息
     * 隐藏密码，拼接头像等
     *
     * @param user 未处理的user
     */
    @Override
    public void preprocessingUser(User user) {
        // 抹去密码
        user.setUserPassword("******");

        // 拼接用户头像URL
        if (user.getUserAvatar() != null) {
            user.setUserAvatarURL(userAvatar.getCommonAddress() + user.getUserAvatar());
        }
    }

    @Override
    public User getFromEmail(String email) {
        User conditionUser = new User();
        conditionUser.setUserEmail(email);
        QueryWrapper<User> wrapper = new QueryWrapper<>(conditionUser);
        User user = userMapper.selectOne(wrapper);
        if (user != null) {
            // 拼接用户头像URL
            if (user.getUserAvatar() != null) {
                user.setUserAvatarURL(userAvatar.getCommonAddress() + user.getUserAvatar());
            }
        }
        return user;
    }

    @Override
    public User register(String email) {

        User user = new User();
        // 设置默认的用户名
        user.setUserUsername(GuoUtilsRandom.getRandomString(51) + new Date().getTime());
        // 设置默认的密码
        user.setUserPassword(null);
        // 设置邮箱
        user.setUserEmail(email);
        // 设置默认手机号
        user.setUserPhone(null);
        // 设置注册时间 yyyy-MM-dd HH:mm:ss
        user.setUserRegisterTime(GuoUtilsDate.getDateNow());
        // 设置默认的昵称
        user.setUserNickname("奶盒#" + GuoUtilsRandom.getRandomString(6));
        // 设置默认头像图片名
        user.setUserAvatar(userAvatar.getDefaultName());
//        // 设置默认不为管理员
//        user.setUserAdmin("false");
        // 设置默认用户角色
        user.setUserRoleId(1);

        log.debug("准备将待注册的用户信息写入数据库：" + user);
        int result = userMapper.insert(user);
        if (result == 1) {
            user.setUserPassword("******");
            log.debug("用户插入成功：" + user);
            return user;
        } else {
            log.error("数据库报错");
            return null;
        }
    }

    @Override
    public Boolean saveUserAvatar(String token, MultipartFile avatar) {
        log.debug("准备保存用户头像...");

        User user = getUserFromRedis(token);
        log.debug("用户唯一标识：" + user.getUserId());

        String contentType = avatar.getContentType();
        log.debug("头像文件类型：" + contentType);
        // 判断是否为image类型，这里不会发生空指针异常，因为在controller中空值是传不进来的会被异常捕获
        if (!"image".equals(contentType.split("/")[0])) {
            log.error("头像文件类型不支持");
            return false;
        }
        String avatarFileExtension = "." + contentType.split("/")[1];

        // 生成新的文件名
        String newAvatarFileName =
                "avatar_" + user.getUserId() + "_" + new Date().getTime() + avatarFileExtension;
        // 生成新的头像文件的磁盘地址
        String newAvatarDiskAddress = userAvatar.getDiskAddress() + newAvatarFileName;
        log.debug("新生成的文件磁盘地址：" + newAvatarDiskAddress);

        try {
            // 保存图片
            log.debug("正在保存...");
            avatar.transferTo(new File(newAvatarDiskAddress));
            log.debug("头像文件保存成功，正在更新数据库...");

            // 更新数据库
            UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
            updateWrapper.eq("user_id", user.getUserId());
            updateWrapper.set("user_avatar", newAvatarFileName);
            int update = userMapper.update(null, updateWrapper);
            if (update != 1) {
                log.error("用户头像信息数据库更新失败");
                return false;
            }
            log.debug("数据库更新成功，正在更新redis...");

            // 更新redis
            String oldAvatarFileName = user.getUserAvatar(); // 更新前先保存原来的名字，后面要把这个图片弃用
            user.setUserAvatar(newAvatarFileName);
            setUserToRedis(token, user);
            log.debug("redis更新完成，正在弃用旧的用户头像...");

            // 校验用户头像是否为默认，如果是默认头像则不启用
            if (user.getUserAvatar() == null ||
                    userAvatar.getDefaultName().equals(oldAvatarFileName)) {
                log.debug("用户头像是默认的，不需要弃用");
            } else {
                // 弃用旧的用户头像（不用必须操作成功）
                String oldPath = userAvatar.getDiskAddress() + oldAvatarFileName;
                String newPath = userAvatar.getAbandon() + oldAvatarFileName;
                log.debug("文件：" + oldPath + "，移动到：" + newPath);
                // 移动
                if (!GuoUtilsFile.removeFile(oldPath, newPath)) {
                    log.warn("旧头像失败，请检查目标文件夹是否存在，以及目标文件夹下是否有重名文件。这个操作不是必须的");
                }
            }

            log.debug("保存头像的所有操作完成，保存成功");

            return true;
        } catch (IOException e) {
            log.error("保存失败，请检查文件名或文件路径");
            throw new RuntimeException(e);
        }
    }

    @Override
    public Boolean resetUserAvatar(String token) {
        User user = getUserFromRedis(token);
        // 校验用户头像是否为默认
        if (user.getUserAvatar() == null ||
                userAvatar.getDefaultName().equals(user.getUserAvatar())) {
            log.debug("用户头（" + user.getUserAvatar() + "）像原来就是默认的，不需要重置");
            return true; // 注意，这里的操作是成功的
        }

        // 将非默认头像移动到弃用目录（不用必须操作成功）
        String oldPath = userAvatar.getDiskAddress() + user.getUserAvatar();
        String newPath = userAvatar.getAbandon() + user.getUserAvatar();
        log.debug("文件：" + oldPath + "，移动到：" + newPath);

        // 移动
        if (!GuoUtilsFile.removeFile(oldPath, newPath)) {
            log.warn("旧头像失败，请检查目标文件夹是否存在，以及目标文件夹下是否有重名文件。这个操作不是必须的");
        }

        // 修改数据库头像字段为默认头像
        UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq("user_id", user.getUserId());
        updateWrapper.set("user_avatar", userAvatar.getDefaultName());
        int update = userMapper.update(null, updateWrapper);
        if (update != 1) {
            log.error("用户头像信息数据库更新失败");
            return false;
        }

        // 更新session信息
        user.setUserAvatar(userAvatar.getDefaultName()); // 修改为默认头像
        setUserToRedis(token, user);

        // 全部操作成功
        log.debug("用户头像重置成功");
        return true;
    }

    @Override
    public Boolean changeUserNickname(String token, String userNickname) {
        User user = getUserFromRedis(token);
        log.debug("原昵称：" + user.getUserNickname() + "。新昵称：" + userNickname);

        // 修改数据库中的用户昵称
        UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq("user_id", user.getUserId());
        updateWrapper.set("user_nickname", userNickname);
        int update = userMapper.update(null, updateWrapper);
        if (update != 1) {
            log.error("用户昵称更新失败，请检查字符串长度是否符合数据库要求！");
            return false;
        }

        // 更新session
        user.setUserNickname(userNickname);
        setUserToRedis(token, user);
        log.debug("昵称修改成功");
        return true;
    }

    /**
     * 从redis缓存中获取用户的信息
     * 1通过token字符串查询redis，查看redis中是否有这一条数据，如果没有则证明当前设备未登录
     * 2如果有这一条数据，则从token字符串中截取出userId
     * 3通过userId查询redis，查询出用户对象的JSON字符串，如果没有则证明当前用户未登录
     * 4更新token中的时间戳
     * 5如果有则将JSON转换为user对象并返回
     *
     * @param token 设备的token
     * @return 返回null表示用户未登录，返回非空则用户已登录
     */
    @Override
    public User getUserFromRedis(String token) {
        if (token == null) {
            return null;
        }
        User user;
        // 操作字符串类型的对象
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        // 通过token字符串查询redis，查看redis中是否有这一条数据，如果没有则证明当前设备未登录
        if (opsForValue.get(token) == null) {
            log.debug("用户未在当前设备登录");
            return null; // 用户未在当前设备登录
        }
        // 则从token字符串中截取出userId
        String userId = token.split(tokenUserInfo.getTokenSplit())[1];
        // 通过userId查询redis，查询出用户对象的JSON字符串，如果没有则证明当前用户未登录
        String userJson = opsForValue.get(
                tokenUserInfo.getUserPrefix() + tokenUserInfo.getUserSplit() + userId);
        if (userJson == null) {
            log.debug("用户未在任何设备登录");
            return null; // 用户未在任何设备登录
        }
        // 更新token的时间戳
        String timestamp = "" + new Date().getTime();
        opsForValue.set(token, timestamp, tokenUserInfo.getTokenRedisTimeout(), TimeUnit.DAYS);
        log.debug("设备（" + token + "）最后活跃时间戳已更新：" + timestamp);

        // 将JSON转换为user对象并返回
        try {
            user = objectMapper.readValue(userJson, User.class);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        log.debug("用户已登录：" + user.toString());
        return user;
    }

    /**
     * 生成token字符串
     *
     * @param userId 用户主键
     * @return 返回token字符串
     */
    @Override
    public String generateToken(Integer userId) {
        return tokenUserInfo.getTokenPrefix() + tokenUserInfo.getTokenSplit()
                + userId + tokenUserInfo.getTokenSplit()
                + GuoUtilsRandom.getRandomString(6);
    }

    /**
     * 生成一个key，这个key是在redis中存放user的
     *
     * @param userId userID
     * @return String
     */
    @Override
    public String generateUserKey(String userId) {
        return tokenUserInfo.getUserPrefix() + tokenUserInfo.getUserSplit() + userId;
    }

    /**
     * 设置用户信息到Redis缓存
     * <p>
     * 1抹去密码，拼接头像，或其他后续功能
     * <p>
     * 2从redis中查询所有以"t_用户Id_"为前缀的键，将这些键封装到map中，判断键的数量
     * <p>
     * 3（if）如果map的数量小于2或者map中存在token这个键，则不需要下线任何设备。
     * 此时不需要进行其他操作
     * <p>
     * 4（else）如果是其他情况，说明当前已经有2个设备登录了，又来了一个新的设备，这时就需要下线一个设备
     * 找出map中时间戳ascii码值最小的那个对应的键，即最早登录的设备的token
     * 然后将这个token从redis中删除
     * <p>
     * 最后
     * 以token为键时间戳为值存入redis并设置其过期时间为7天，如果这个token已经存在于redis中
     * 则会自动覆盖旧的时间戳。
     * 然后以userId为键user对象的JSON字符串为值存入redis中并设置其过期时间为7天，
     * 如果redis中已经存在这个键，同样会覆盖，这样就会更新用户在redis中的信息。
     *
     * @param token 当前设备的token
     * @param user  要存入的user对象
     * @return 返回0表示设备过多有设备被挤退下线，返回1表示设备已经登录过了，
     * 返回2表示设备数量未达上线且当前设备还没有登录
     */
    @Override
    public int setUserToRedis(String token, User user) {
        int flag;

        // 预处理用户
        preprocessingUser(user);

        // 从redis中查询所有以"t_用户Id_"为前缀的键，将这些键封装到map中，判断键的数量
        Set<String> likeKeySet = stringRedisTemplate.keys(
                tokenUserInfo.getTokenPrefix() + tokenUserInfo.getTokenSplit() +
                        user.getUserId() + tokenUserInfo.getTokenSplit() + "*");
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        HashMap<String, String> map = new HashMap<>();
        if (likeKeySet != null && likeKeySet.size() != 0) {
            for (String key : likeKeySet) {
                map.put(key, opsForValue.get(key));
            }
        }
        log.debug("查询到的所有设备：" + map);
        log.debug("当前配置文件中限制登录设备数量：" + tokenUserInfo.getDeviceQuantityLimit());
        if (map.size() >= tokenUserInfo.getDeviceQuantityLimit()
                && !likeKeySet.contains(token)) { // 只要限制数量大于0，一般就不会出现空指针
            // 当前已经有2个设备登录了，需要下线一个设备
            log.debug("允许设备登录数量超过上线，正在下线最早登录的设备...");

            // 找出map中时间戳ascii码值最小的那个对应的键，即最早登录的设备的token
            String minKey = GuoUtilsSort.getMinKeyFromMapStringString(map);

            // 然后将这个token从redis中删除，将新的token存入redis并设置其过期时间
            stringRedisTemplate.delete(minKey);
            log.debug("设备已下线：" + minKey);
            flag = 0;
        } else if (map.size() > 0 && likeKeySet.contains(token)) {
            log.debug("设备登录数量未达上限，且当前设备已经登录过了");
            flag = 1;
        } else {
            log.debug("设备登录数量未达上限，且当前设备未登录");
            flag = 2;
        }

        log.debug("正在插入（更新）token和user...");
        // 以token为键时间戳为值存入redis并设置其过期时间为7天
        String timestamp = "" + new Date().getTime();
        opsForValue.set(token, timestamp, tokenUserInfo.getTokenRedisTimeout(), TimeUnit.DAYS);
        log.debug("存入redis：“" + token + ":" + timestamp + "”");
        // 以userId为键user对象的JSON字符串为值存入redis中并设置其过期时间为7天
        try {
            String userJson = objectMapper.writeValueAsString(user);
            String userKey = generateUserKey(user.getUserId().toString());
            opsForValue.set(userKey, userJson, tokenUserInfo.getUserRedisTimeout(), TimeUnit.DAYS);
            log.debug("存入redis：“" + userKey + ":" + userJson + "”");
        } catch (JsonProcessingException e) {
            log.error("JSON转换异常");
            throw new RuntimeException(e);
        }

        log.debug("设置成功");
        return flag;
    }

    /**
     * 将token写入cookie，将cookie写入response
     *
     * @param token    token
     * @param response response
     */
    @Override
    public void setTokenToCookieToResponse(String token, HttpServletResponse response) {
        Cookie cookie = new Cookie("token", token);
        cookie.setPath("/");
        cookie.setMaxAge(60 * 60 * 24 * tokenUserInfo.getTokenClientTimeout());
        response.addCookie(cookie);
    }

    /**
     * 让客户端删除cookie中的token
     * 创建一个新的token，存活时长为0，覆盖掉原来的就可以实现删除操作了
     *
     * @param response response
     */
    @Override
    public void removeTokenCookieFromResponse(HttpServletResponse response) {
        Cookie cookie = new Cookie("token", null);
        cookie.setPath("/");
        cookie.setMaxAge(0);
        response.addCookie(cookie);
    }

    /**
     * 移除当前用户的当前设备的redis中的信息
     * 只移除token对应的用户时间戳值，Redis中的user对象不受影响
     * 如果只有当前一个设备登录，则同时将user对象删除
     *
     * @param token 当前设备的token
     */
    @Override
    public void removeUserFromRedisForCurrentDevice(String token) {
        // 将token删除
        stringRedisTemplate.delete(token);
        log.debug("当前设备已退出：" + token);
        // 获取userId
        String userId = token.split(tokenUserInfo.getTokenSplit())[1];
        Set<String> keys = stringRedisTemplate.keys(
                tokenUserInfo.getTokenPrefix() + tokenUserInfo.getTokenSplit() +
                        userId + tokenUserInfo.getTokenSplit() + "*");
        if (keys == null || keys.size() == 0) { // 如果只有当前一个设备登录，则同时将user对象删除
            log.debug("只有当前一个设备登录，则同时将user对象删除");
            stringRedisTemplate.delete(generateUserKey(userId));
        }
    }

    /**
     * 移除当前用户的所有设备的redis中的信息
     * token和user信息全部都要删除
     *
     * @param token 当前设备的token
     */
    @Override
    public void removeUserFromRedisForAllDevice(String token) {
        // 获取userId
        String userId = token.split(tokenUserInfo.getTokenSplit())[1];
        Set<String> keys = stringRedisTemplate.keys(
                tokenUserInfo.getTokenPrefix() + tokenUserInfo.getTokenSplit() +
                        userId + tokenUserInfo.getTokenSplit() + "*");
        if (keys != null && keys.size() > 0) {
            stringRedisTemplate.delete(keys);
            log.debug("token被清除：" + keys);
        }
        String userKey = generateUserKey(userId);
        stringRedisTemplate.delete(userKey);
        log.debug("用户" + userKey + "下线成功");
    }
}
